home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Language/OS - Multiplatform Resource Library
/
LANGUAGE OS.iso
/
pcl
/
cl-nd-cl.lha
/
clue
/
clio
/
dlog-button.lisp
< prev
next >
Wrap
Lisp/Scheme
|
1990-07-31
|
46KB
|
1,105 lines
;;; -*- Mode:Lisp; Package:CLIO-OPEN; Base:10; Lowercase:T; Syntax:Common-Lisp -*-
;;;----------------------------------------------------------------------------------+
;;; |
;;; TEXAS INSTRUMENTS INCORPORATED |
;;; P.O. BOX 149149 |
;;; AUSTIN, TEXAS 78714 |
;;; |
;;; Copyright (C) 1989, 1990 Texas Instruments Incorporated. |
;;; |
;;; Permission is granted to any individual or institution to use, copy, modify, and |
;;; distribute this software, provided that this complete copyright and permission |
;;; notice is maintained, intact, in all copies and supporting documentation. |
;;; |
;;; Texas Instruments Incorporated provides this software "as is" without express or |
;;; implied warranty. |
;;; |
;;;----------------------------------------------------------------------------------+
;;;
;;; Dialog-button and dialog-item, buttons and items that bring up general
;;; dialogs when messed with. Dialogs include menus, property-sheets, commands,
;;; and confirms. Also defined here is the menu protocol event translations,
;;; for press-drag-release and click-move-click.
(in-package "CLIO-OPEN")
(export '(
dialog-button
dialog-item
make-dialog-item
make-dialog-button
button-dialog
present-dialog ; As good a place as any.
)
'clio-open)
;;;
;;; Contact definitions and interface functions.
(defcontact dialog-button (action-button)
((dialog :type (or null list function contact)
:reader button-dialog ; Note (setf button-dialog) below.
:initarg :dialog
:initform nil))
(:resources
(dialog :initform nil
:type (or null list function contact))))
(defun make-dialog-button (&rest initargs)
(apply #'make-contact 'dialog-button initargs))
(defmethod (setf button-dialog) (new-dialog (button dialog-button))
(check-type new-dialog (or null contact))
(with-slots (preferred-width dialog) button
;; (LG) Force preferred-size to recalculate width.
(setq preferred-width nil)
(when (and dialog
(not (eq dialog new-dialog)))
(disassociate-dialog-from-button dialog button)))
(associate-dialog-with-button new-dialog button))
;; A DIALOG-ITEM is a specialization of an ACTION-ITEM and is intended for use
;; in OL compliant menus. It differs from a DIALOG-BUTTON in appearance as well
;; as in its sensitivity to various mouse gestures depending on the mode of the
;; menu which contains it.
(defcontact dialog-item (action-item)
((dialog :type (or null list function contact)
:reader button-dialog ; Note (setf button-dialog) below.
:initarg :dialog
:initform nil)
(last-x :type integer ; For drag-right checking.
:initform 0)
(active-x :type (or null integer) ; Ditto.
:initform nil))
(:resources
(dialog :initform nil
:type (or null list function contact))))
(defun make-dialog-item (&rest initargs)
(apply #'make-contact 'dialog-item initargs))
(defmethod (setf button-dialog) (new-dialog (item dialog-item))
(check-type new-dialog (or null contact))
(with-slots (preferred-width dialog) item
;; (LG) Force preferred-size to recalculate width.
(setq preferred-width nil)
(when (and dialog
(not (eq dialog new-dialog)))
(disassociate-dialog-from-button dialog item)))
(associate-dialog-with-button new-dialog item))
(defmethod resize :after ((item dialog-item) width height border-width)
(declare (ignore width height border-width))
(with-slots (active-x) item
(setq active-x nil)))
;;;
;;; Other definitions.
;; A way to get from the dialog back to the button.
(defmacro button-owning-dialog (contact)
`(getf (window-plist ,contact) 'button-owning-dialog))
(defun pointer-inside-menu-p (button menu)
(declare (ignore button))
(multiple-value-bind (pointer-x pointer-y same-screen-p)
(pointer-position menu)
(and same-screen-p (inside-contact-p menu pointer-x pointer-y))))
;; A handy place to put the state (nil, press-drag-release, or click-move-click).
(defmacro menu-state (menu)
`(getf (window-plist ,menu) 'menu-state))
;; Flag used to handle off-menu presses and releases.
(defmacro menu-button-press-p (menu)
`(getf (window-plist ,menu) 'menu-button-press-p))
(defparameter *menu-item-drag-right-distance* 5
"Distance in pixels to drag the pointer rightward over a menu item
to bring up a submenu.")
(defparameter *menu-cursor-index* top-left-arrow-cursor ; (That's 132.)
"Index of glyph used for pointer cursor when grabbed by menu.")
;; Flag used to prevent multiple drag-mode submenus from appearing.
(defmacro menu-present-in-progress (container)
`(getf (window-plist ,container) 'present-dialog-in-progress))
;;;
;;; Initialisation code.
;; Allow for a class-name-symbol or list of class-name and initargs by
;; parsing the :dialog initarg and making it a contact before passing it
;; on to the rest of the init method.
(defmethod initialize-instance :around ((self dialog-button) &rest initargs &key dialog parent)
(let ((new-dialog (parse-dialog-spec dialog parent)))
(apply #'call-next-method self :dialog new-dialog initargs)))
(defmethod initialize-instance :around ((self dialog-item) &rest initargs &key dialog parent)
(let ((new-dialog (parse-dialog-spec dialog parent)))
(apply #'call-next-method self :dialog new-dialog initargs)))
(defun parse-dialog-spec (spec parent)
(etypecase spec
((or contact null) spec)
((or symbol function) (funcall spec :parent parent))
(list (apply (car spec) :parent parent (cdr spec)))))
(defmethod initialize-instance :after ((self dialog-button) &key &allow-other-keys)
(associate-dialog-with-button (button-dialog self) self))
(defmethod initialize-instance :after ((item dialog-item) &key &allow-other-keys)
(associate-dialog-with-button (button-dialog item) item))
(defmethod associate-dialog-with-button ((new-dialog t) button)
(with-slots (dialog) (the dialog-button button)
(setq dialog new-dialog)))
(defmethod associate-dialog-with-button :after ((new-dialog menu) button)
(associate-menu-with-dialog-button new-dialog button))
;; These dialogs use callback instead of event because the action is supposed to happen on release,
;; while pressing just highlights. The callback works, because that's what action-button
;; is doing.
(defmethod associate-dialog-with-button :after ((new-dialog command) button)
(add-callback button :release #'(lambda ()
(present-dialog (button-dialog button)))))
(defmethod associate-dialog-with-button :after ((new-dialog confirm) button)
;; A bit of a hack for confirm: We don't want the menu to dismiss until the
;; the confirm does, so if there's a dismiss-menu callback (indicating that
;; our owning button is within a menu), we remove it, extracting its menu
;; argument, and put it on the confirm's :accept and :cancel callbacks instead.
(let* ((off-callbacks (callback-p button :off))
(dismiss-callback (assoc #'dismiss-menu off-callbacks)))
(when dismiss-callback
(delete-callback button :off #'dismiss-menu) ; Move from here ...
(when (typep button 'toggle-button)
(delete-callback button :off #'dismiss-menu))
(add-callback new-dialog :cancel #'dismiss-menu (second dismiss-callback)) ; ... to here.
(add-callback new-dialog :accept #'dismiss-menu (second dismiss-callback))))
(setf (button-owning-dialog new-dialog) button)
(add-callback button :release #'(lambda ()
(setf (confirm-near (button-dialog button))
(viewable-ancestor button))
(present-dialog (button-dialog button)))))
(defmethod associate-dialog-with-button :after ((new-dialog property-sheet) button)
(add-callback button :release #'(lambda ()
(present-dialog (button-dialog button)))))
(defun associate-menu-with-dialog-button (menu button)
(declare (type menu menu)
(type (or dialog-button dialog-item) button))
;; Make-menu handles associating dismiss-menu with :on and :off callbacks
;; on each item. This :unmap callback handles taking down submenus and
;; doing choice-item-release when the menu is withdrawn by dismiss-menu.
(add-callback menu :unmap #'dismiss-menu-group menu button)
;; Remember owning button for later use in event-handlers.
(setf (button-owning-dialog menu) button))
(defmethod disassociate-dialog-from-button ((dialog menu) button)
(disassociate-menu-from-dialog-button dialog button))
(defmethod disassociate-dialog-from-button ((dialog command) button)
(delete-callback button :release))
(defmethod disassociate-dialog-from-button ((dialog confirm) button)
(add-callback dialog :cancel #'dismiss-menu)
(add-callback dialog :accept #'dismiss-menu)
(setf (button-owning-dialog dialog) nil)
(delete-callback button :release))
(defmethod disassociate-dialog-from-button ((dialog property-sheet) button)
(delete-callback button :release))
(defmethod disassociate-dialog-from-button ((dialog null) button)
(declare (ignore button))
nil)
(defun disassociate-menu-from-dialog-button (menu button)
(declare (type menu menu)
(ignore button))
(setf (button-owning-dialog menu) nil)
(delete-callback menu :unmap #'dismiss-menu-group))
;; Hook for an off-menu-press problem: when leaving an item, turn off the
;; off-menu-press flag so an off-menu-release won't dismiss the menu, because
;; the press was within an item, not off the menu. Also a hook for a confirm-
;; related grab problem: when firing an action-item, ungrab the pointer and
;; set the menu-state to a special state, finishing, that just ignores enter
;; and leave events on the menu. We need to do this for items whose callbacks
;; call confirm-p or some similar dialog-presenting function, so the dialog
;; gets a chance to get button presses and releases.
(defmethod add-menu-item-callbacks :after (item menu)
(add-callback item :canceling-change
#'(lambda (to-selected-p)
(declare (ignore to-selected-p))
(setf (menu-button-press-p menu) nil)))
(add-callback item :release #'(lambda ()
(setf (menu-state menu) 'finishing)
(ungrab-pointer (contact-display menu))
)))
(defun viewable-ancestor (contact)
(let ((parent (typecase contact
(menu
(button-owning-dialog contact))
(shell
(shell-owner contact))
(otherwise
(contact-parent contact)))))
(if (typep parent 'root)
contact
(let ((ancestor (viewable-ancestor parent)))
(if (and (mapped-p contact)
(eq ancestor parent))
contact
ancestor)))))
;;;
;;; Action functions for dialog-button and dialog-item.
;; Present-dialog methods for other dialogs are in their respective files.
;; This method starts the menu protocol defined below in the event handlers,
;; and sets position according to the complicated Open Look rules.
(defmethod present-dialog ((menu menu) &key x y button state)
(declare (type (or card16 null) x y))
(declare (ignore x y)) ; Stick to Open Look positioning rules.
(check-type button (or (member :button-1 :button-2 :button-3 :button-4 :button-5) null))
(check-type state (or mask16 null))
(let ((owning-button (button-owning-dialog menu)))
(cond (owning-button
(set-menu-position owning-button menu
(and button state
(not (logtest (make-state-mask button) state)))))
(:else
;; No button, this is a pop-up menu.
(set-menu-position nil menu
(and button state
(not (logtest (make-state-mask button) state))))
(associate-menu-with-dialog-button menu nil)
;; Need this to do the initial grab-handoff to the menu so we can
;; start popups in press-drag-release -- a quick enough button-release
;; will switch to click-move-click, but I'm not sure of the mechanism.
;; Need to do it as a callback because we can't grab until we're mapped,
;; and that doesn't happen immediately.
(add-callback menu :map
#'(lambda ()
(ungrab-pointer (contact-display menu))
(grab-pointer menu #.(make-event-mask :button-release :enter-window :leave-window)
:owner-p t
:cursor (contact-glyph-cursor menu *menu-cursor-index*))))))
(setf (contact-state menu) :mapped)
(setf (menu-state menu) nil)))
;; Default case, just position it and map it (this method handles commands and
;; property-sheets, but not confirms or menus).
(defmethod present-dialog ((contact contact) &key x y button state)
(declare (type (or (member :button-1 :button-2 :button-3 :button-4 :button-5) null) button)
(type (or mask16 null) state))
(declare (ignore button state))
(check-type x (or card16 null))
(check-type y (or card16 null))
(unless (or x y)
(multiple-value-setq (x y)
(pointer-position (contact-parent contact))))
(change-geometry contact :x x :y y :accept-p t)
(setf (contact-state contact) :mapped))
;; This function is called in the :unmap callback of a menu, which dismiss-menu
;; causes to happen by withdrawing the menu. Other cleanup, like taking down any
;; submenus and releasing the button or item, happens here.
(defun dismiss-menu-group (menu button)
;; If there are any submenus up, take them down, too.
(mapc #'dismiss-submenu-item
(composite-children (menu-choice menu)))
(when button
(setf (menu-present-in-progress (contact-parent button)) nil))
(when (and button ; Button will be NIL for pop-up.
;; Special state, only during leave-notify of menu when exiting
;; to left, which means take down the menu but not any superiors.
(not (eq (menu-state menu) 'exiting-to-left)))
;; NOTE we defer the "release" of the button until the associated
;; menu is dismissed. We do this because the menu button will
;; normally never see the actual release event. Note also that,
;; as an action button, the :ON callback is not invoked until the
;; release method is invoked.
(choice-item-release button)
(release-select button)))
(defmethod dismiss-submenu-item (item)
(declare (ignore item))
nil)
(defmethod dismiss-submenu-item ((item dialog-item))
(with-slots (dialog) item
(when (and (typep dialog 'menu)
(mapped-p dialog))
(dismiss-menu dialog))))
;; Used to dismiss any dialogs active under a given menu, when bringing
;; up a different dialog from that menu.
(defmethod dismiss-active-dialogs (item)
(declare (ignore item))
nil)
(defmethod dismiss-active-dialogs ((item dialog-item))
(with-slots (dialog) item
(when (mapped-p dialog)
;; This flag, originally used when taking down drag-mode menus by
;; exiting to the left, here is used to prevent superior menus of this
;; one from being taken down.
(when (typep dialog 'menu)
(setf (menu-state dialog) 'exiting-to-left))
(setf (contact-state dialog) :withdrawn)
(display-button-unhighlighted item))))
;; Special methods for dialog-button because we need to display the default
;; on press and select it on release. The special stuff will only be called
;; when the dialog is a menu, the others will just call the next method and
;; get the action-button normal stuff. I'd like to do this in an :after
;; method or some other cleaner way, but I need to do this all inside the
;; conditional, and I'm not sure how to tell whether it was true.
(DEFMETHOD press-select ((dialog-button dialog-button))
(with-slots (dialog) dialog-button
(if (typep dialog 'menu)
(press-select-show-default dialog-button dialog)
(call-next-method))))
(DEFMETHOD press-select ((dialog-item dialog-item))
(with-slots (dialog) dialog-item
(if (typep dialog 'menu)
(press-select-show-default dialog-item dialog)
(call-next-method))))
(defun press-select-show-default (dialog-button dialog)
(declare (type action-button dialog-button)) ; Both dialog-item and dialog-button are.
(with-event (x y)
(WHEN (and (inside-contact-p dialog-button x y)
(choice-item-press dialog-button))
;; Show the default value in the button.
(with-slots (font label fill-color foreground last-displayed-as width height) dialog-button
(LET* ((scale (contact-scale dialog-button))
(choice (menu-choice dialog))
(default (or (choice-default choice) ; Could be NIL, but Open Look insists.
(first (composite-children choice))))
(ab-foreground foreground)
(ab-fill-color fill-color)
(ab-font font)
(dims (getf *button-dimensions-by-scale* scale))
(text-x (ab-left-button-end-width dims))
(text-y (1+ (ab-text-baseline dims)))) ; 0+ for dialog-item.
;; Experiment: try changing the label and redisplaying. Problems: doesn't
;; suppress the menu mark, doesn't show the more-text-to-right gray arrow.
; (with-slots (label) dialog-button
; (let ((old-label label))
; (unwind-protect
; (progn (setq label (button-label default))
; (redisplay-button dialog-button))
; (setq label old-label))))
;; Avoid error on abbreviated buttons -- interior width ends up negative.
;; This way, we just highlight and don't even try to show the default.
(unless (< width (+ (ab-left-button-end-width dims)
(ab-right-button-end-width dims)))
(using-gcontext
(gc
:drawable dialog-button
:foreground ab-foreground
:background ab-fill-color
:font ab-font)
(just-clear-body-of-button dialog-button gc))
(using-gcontext
(gc
:drawable dialog-button
:foreground ab-fill-color
:background ab-foreground
:font ab-font)
(let ((default-label (button-label default)))
(if (stringp default-label)
(display-constrained-text
dialog-button gc default-label ab-font
(label-width dialog-button label)
:x text-x :y text-y)
(let*
((label-width (label-width dialog-button default-label))
(label-height (getf (pixmap-plist default-label) :height)))
(with-gcontext (gc :fill-style :tiled :tile default-label)
(draw-rectangle
dialog-button gc
text-x (max 0 (pixel-round (- height label-height) 2))
label-width label-height t))))))))))))
(DEFUN display-constrained-text (contact gc text font available-width &key (x 0) (y 0))
(LET* ((more-arrow (GETF *more-text-arrows-by-scale* (contact-scale contact)))
(more-arrow-image (more-text-arrow-image more-arrow))
(more-arrow-width (image-width more-arrow-image))
)
(FLET
((get-displayable-width-of-text (text font available-width)
;; Returns (<#-of-chars-in-text>) if entire text fits.
;; Returns (<#-of-displayable-chars> <npixels-displayable>) if not.
(IF (<= (text-width font text) available-width)
(LENGTH text)
;; else we gotta figure out how many chars will fit.
;; Since text-width is a very expensive function we're going to try to get an estimate
;; for where in the text we get too wide to fit before we start calling it.
(DO* ((reduced-space-for-text (- available-width more-arrow-width))
(est-displayable-length (FLOOR reduced-space-for-text (max-char-width font)))
(i (1+ est-displayable-length) (1+ i))
(test-width)
(last-test-width (text-width font text :end est-displayable-length) test-width)
)
((>= i (LENGTH text)))
(SETF test-width (text-width font text :end i))
(WHEN (> test-width reduced-space-for-text)
(RETURN (VALUES (1- i) last-test-width))))))
)
;; Get the # of characters that fit (and their width if truncating)...
(MULTIPLE-VALUE-BIND (displayable-length-of-text displayable-width-of-text)
(get-displayable-width-of-text text font available-width)
;; Draw the characters that we can...
(draw-glyphs contact gc x y text :end displayable-length-of-text)
;; If the entire label would not fit, place a More Text Arrow to the right of it...
;; We assume here that the pixmap for this scale's More Text Arrow has already been
;; cached so contact-mask can pick it up...
(WHEN displayable-width-of-text
(LET* ((more-arrow-x (+ x displayable-width-of-text
(more-text-arrow-offset-from-text more-arrow)))
(more-arrow-y (+ y (more-text-arrow-offset-from-baseline more-arrow)))
(more-arrow-pixmap (contact-image-mask contact more-arrow-image :depth 1)))
(with-gcontext (gc :clip-x more-arrow-x
:clip-y more-arrow-y
:clip-mask more-arrow-pixmap)
(draw-rectangle contact gc more-arrow-x more-arrow-y
more-arrow-width (image-height more-arrow-image) t))))))))
(DEFMETHOD release-select ((dialog-button dialog-button))
(with-slots (dialog) dialog-button
(if (typep dialog 'menu)
(release-select-choose-default dialog-button dialog)
(call-next-method))))
(DEFMETHOD release-select ((dialog-item dialog-item))
(with-slots (dialog) dialog-item
(if (typep dialog 'menu)
(release-select-choose-default dialog-item dialog)
(call-next-method))))
(defun release-select-choose-default (dialog-button dialog)
(with-slots (last-displayed-as) (the dialog-button dialog-button)
;; Do nothing unless highlighted/selected already...
(WHEN (EQ last-displayed-as :highlighted)
(let ((ultimate-default (find-ultimate-default (menu-choice dialog))))
(choice-item-press ultimate-default)
(choice-item-release ultimate-default)
(choice-item-release dialog-button)))))
(defun find-ultimate-default (choice)
(let ((default (or (choice-default choice) ; Could be NIL, but Open Look insists on a default.
(first (composite-children choice)))))
(typecase default
((or dialog-button dialog-item)
(let ((dialog (button-dialog default)))
(if (typep dialog 'menu)
(find-ultimate-default (menu-choice dialog))
default)))
(otherwise
default))))
;;;
;;; Event translations for dialog-button/item and menus.
;;;
;;; These implement a sort of state machine. The components of the current state
;;; are the state of dialog (:mapped or not), the type of the dialog (menus behave
;;; differently than other dialogs), and the menu-state of the menu (nil,
;;; press-drag-release, or click-move-click). Mostly they use the type to decide
;;; their sensitivity to the event, the state of the dialog to determine whether
;;; this is the first time for this event (for example, startup should only happen
;;; once), and the menu-state to differentiate between modes for grabbing purposes.
;;;
;;; Dialog button translations.
(defevent dialog-button
(:button-press :button-3)
dialog-button-do-startup)
(defun dialog-button-do-startup (button)
(let ((dialog (button-dialog button)))
(when (and (typep dialog 'menu)
(not (mapped-p dialog))
(choice-item-press button))
;; Present-dialog on menu sets menu-state to nil.
(present-dialog dialog :button :button-3 :state (with-event (state) state)))))
(defevent dialog-button
(:button-release :button-3)
dialog-button-button-3-release)
(defun dialog-button-button-3-release (button)
(let ((dialog (button-dialog button)))
(when (and (typep dialog 'menu)
(mapped-p dialog)
(null (menu-state dialog)))
;; Menu just brought up by preceding press, go into click-move-click mode.
(display-action-button-busy button)
(grab-pointer dialog #.(make-event-mask :button-press :button-release :enter-window)
:owner-p t
:cursor (contact-glyph-cursor dialog *menu-cursor-index*))
(setf (menu-state dialog) 'click-move-click))))
(defevent dialog-button
:leave-notify
dialog-button-leave-notify)
(defun dialog-button-leave-notify (button)
(declare (type dialog-button button))
(let ((dialog (button-dialog button)))
(if (and (typep dialog 'menu)
(mapped-p dialog)
(null (menu-state dialog)))
(with-event (time mode kind root-x root-y)
(when (eq mode :normal)
;; We ungrab the pointer independent of its current location since
;; the menu will be responsible for fielding any release event.
(ungrab-pointer (contact-display button) :time time)
(multiple-value-bind (dialog-x dialog-y)
(contact-translate (contact-root button) root-x root-y dialog)
(if (inside-contact-p dialog dialog-x dialog-y) ; Avoid server round-trip.
(grab-pointer dialog #.(make-event-mask :button-release :enter-window :leave-window)
:owner-p t
:cursor (contact-glyph-cursor dialog *menu-cursor-index*))
(grab-pointer dialog #.(make-event-mask :button-release :enter-window)
:cursor (contact-glyph-cursor dialog *menu-cursor-index*))))
(setf (menu-state dialog) 'press-drag-release)))
(with-slots (last-displayed-as) button
;; Do nothing unless highlighted/selected already...
(WHEN (EQ last-displayed-as :highlighted)
(leave button))))))
;;;
;;; Menu translations.
(defevent menu
:button-press
dialog-button-button-press)
(defun dialog-button-button-press (menu)
(setf (menu-button-press-p menu) t))
(defevent menu
:button-release
dialog-button-dismiss-menu-group)
(defun dialog-button-dismiss-menu-group (menu)
(cond ((null (menu-state menu))
(setf (menu-state menu) 'click-move-click))
((or (menu-button-press-p menu)
(eq (menu-state menu) 'press-drag-release))
(dismiss-menu menu)))
(setf (menu-button-press-p menu) nil))
(defevent menu
:enter-notify
dialog-button-menu-enter-notify)
(defun dialog-button-menu-enter-notify (menu)
(with-event (time mode state)
(flet ((pdr-enter ()
;; First we ungrab the pointer so choice items will get proper
;; event notifications
(ungrab-pointer (contact-display menu) :time time)
(grab-pointer menu #.(make-event-mask :button-release :enter-window :leave-window)
:owner-p t
:cursor (contact-glyph-cursor menu *menu-cursor-index*)))
(cmc-enter ()
(ungrab-pointer (contact-display menu) :time time)
(grab-pointer menu #.(make-event-mask :button-press :button-release)
:owner-p t
:cursor (contact-glyph-cursor menu *menu-cursor-index*))))
(ecase (menu-state menu)
((nil)
;; Pop-up menu, a la SCIFI. Choose mode based on button state.
;; The test below will be T if button-3 is down, meaning we've entered
;; the menu with the button pressed, hence press-drag-release mode. If
;; the button is up, we go to click-move-click.
(cond ((logtest #.(make-state-mask :button-3) state)
(setf (menu-state menu) 'press-drag-release)
(pdr-enter))
(:else
(setf (menu-state menu) 'click-move-click)
(cmc-enter))))
(press-drag-release
(when (eq mode :normal)
(pdr-enter)))
(click-move-click
(when (eq mode :normal)
(cmc-enter)))
(finishing
; (when (eq mode :normal)
; (setf (menu-state menu) 'click-move-click)
; (cmc-enter))
)
(exiting-to-left
;; May happen if we leave a dialog-item before the menu's up
;; and have to take it down again.
nil)))))
(defevent menu
:leave-notify
dialog-button-menu-leave-notify)
(defun dialog-button-menu-leave-notify (menu)
(with-event (time mode x y)
(when (eq mode :normal)
(ecase (menu-state menu)
(press-drag-release
(ungrab-pointer (contact-display menu) :time time)
(let ((button (button-owning-dialog menu)))
(cond ((and (typep button 'dialog-item)
(< x 0)) ; A crude leave-left-edge test for items.
(setf (menu-state menu) 'exiting-to-left) ; Flag for dismiss-menu-group.
(setf (contact-state menu) :withdrawn)
;; +++ I want to do choice-item-leave if the new position isn't within the button.
;; The event coordinates are relative to the menu, though, so how exactly do
;; I translate them? In the meantime, it seems to be better to leave always.
(choice-item-leave button)
)
(:else
(grab-pointer menu #.(make-event-mask :button-release :enter-window)
:cursor (contact-glyph-cursor menu *menu-cursor-index*))))))
(click-move-click
(ungrab-pointer (contact-display menu) :time time)
(grab-pointer menu #.(make-event-mask :button-press :button-release :enter-window)
:cursor (contact-glyph-cursor menu *menu-cursor-index*)))
(exiting-to-left
;; Need this because there'll be another leave-notify during the unmapping.
nil)
(finishing
nil)))))
;;;
;;; Dialog item translations.
(defevent dialog-item
(:button-press :button-3)
choice-item-press)
(defevent dialog-item
(:button-release :button-3)
dialog-item-start-cmc-mode)
(defun dialog-item-start-cmc-mode (item)
(let ((dialog (button-dialog item)))
(when (not (mapped-p dialog))
(cond ((typep dialog 'menu)
;; If there are any dialogs up at this level, take them down.
(mapc #'dismiss-active-dialogs
(composite-children (contact-parent item)))
;; Dialog-item, superior menu in stay-up mode, we fire on the release
;; and bring up the submenu in stay-up mode.
(present-dialog dialog :button :button-3 :state 0)
;; This is dialog-button-button-3-release without the grab-pointer.
(display-action-button-busy item)
(setf (menu-state dialog) 'click-move-click))
(:else
(choice-item-release item))))))
(defevent dialog-item
:leave-notify
leave-dialog-item)
(defun leave-dialog-item (item)
(declare (type dialog-item item))
(with-event (state mode)
(cond ((and (logtest #.(make-state-mask :button-3) state)
(not (mapped-p (button-dialog item))))
;; We set last-x to the right-hand end of the item to force recalculation
;; when we re-enter.
(with-slots (last-x width) (the dialog-item item)
(setq last-x width))
;; We ungrab the pointer independent of its current location since
;; the menu will be responsible for fielding any release event.
(with-event (time mode)
(with-slots (last-displayed-as) item
(when (and (eq mode :normal)
(eq last-displayed-as :highlighted))
(ungrab-pointer (contact-display item) :time time)
(choice-item-leave item)))))
(:else
(with-slots (last-displayed-as) item
;; Do nothing unless highlighted/selected already...
(when (eq last-displayed-as :highlighted)
(leave item)))))))
(defevent dialog-item
:enter-notify
dialog-item-enter-notify)
(defmethod dialog-item-enter-notify ((item dialog-item))
(with-slots (dialog last-x active-x width last-displayed-as) item
(when (and (not (mapped-p dialog)) (eq last-displayed-as :unhighlighted))
(with-event (x y state)
(if (and (inside-contact-p item x y) ; +++ Inactive items don't get enter-notify, remove this?
(logtest #.(make-state-mask :button-3) state)
(or (not (typep dialog 'menu))
(not (menu-present-in-progress (contact-parent item)))) ; Don't allow multiple PDR menus.
;; The pointer has been dragged over this button w/menu button
;; pressed. This has the same side effects as pressing the
;; select button so we go ahead and use the press procedure
;; to take care of visuals and approve the transition.
(choice-item-press item))
;; Transition was approved and button is now highlighted.
;; The choice-item-press is enough for non-menus, but menus have more:
(when (typep dialog 'menu)
(when (null active-x)
(let ((dims (getf *button-dimensions-by-scale* (contact-scale item))))
(setq active-x (- width
(ab-right-button-end-width dims)
(image-width (ab-horizontal-menu-mark-image dims))))))
(setq last-x x)
(when (>= x active-x)
;; Entered in the "submenu region," which is that area from the
;; left edge of the menu mark to the right edge of the item.
;; If there are any dialogs up at this level, take them down.
(mapc #'dismiss-active-dialogs
(composite-children (contact-parent item)))
;; Bring up the menu and go into the submenu protocol.
(present-dialog dialog :button :button-3 :state state)
(setf (menu-present-in-progress (contact-parent item)) t)
; (setf (menu-state dialog) 'press-drag-release)
))
;; Transition not approved, so inhibit the drag-right check on :motion-notify.
(when (typep dialog 'menu)
(setq last-x width)))))))
(defevent dialog-item
:motion-notify
dialog-item-drag-right)
(defmethod dialog-item-drag-right ((item dialog-item))
(with-slots (dialog last-x active-x width) item
(when (and (typep dialog 'menu)
(not (mapped-p dialog))
(not (menu-present-in-progress (contact-parent item)))
active-x) ; Paranoia check.
(with-event (x y state)
(when (and (inside-contact-p item x y)
(logtest #.(make-state-mask :button-3) state))
(cond ((or (>= x active-x)
(> (- x last-x) *menu-item-drag-right-distance*))
;; If there are any dialogs up at this level, take them down.
(mapc #'dismiss-active-dialogs
(composite-children (contact-parent item)))
;; Dragged right far enough, or into active area, bring up menu.
(present-dialog dialog :button :button-3 :state state)
(setq last-x width) ; Force recalculation on later entries.
(setf (menu-state dialog) 'press-drag-release))
((< x last-x) ; Moving left, save leftmost.
(setq last-x x))
(:else ; Moving right, keep old left.
nil)))))))
;;;
;;; Display code. Dialog-buttons and dialog-items show a menu mark or
;;; window mark to the right of the item. These functions and methods
;;; allow space for it and do the drawing.
(defvar *inside-display-window-mark* nil) ; Don't do it inside internal routine.
;; Daemons on the Dialog Button's label manipulation methods to adjust the width
;; of the label for the menu mark and the display the menu mark.
(defmethod label-width :around ((button dialog-button) label)
(if *inside-display-window-mark*
(call-next-method)
(with-slots (dialog) button
(let ((dims (getf *button-dimensions-by-scale* (contact-scale button))))
(+ (call-next-method)
(additional-label-width dialog button dims)
(- (ab-right-button-end-width dims)
2)))))) ; Right border thickness
(defmethod label-width :around ((button dialog-item) label)
(if *inside-display-window-mark*
(call-next-method)
(with-slots (dialog) button
(let ((dims (getf *button-dimensions-by-scale* (contact-scale button))))
(+ (call-next-method)
(additional-label-width dialog button dims)
(- (ab-right-button-end-width dims)
2)))))) ; Right border thickness
(defmethod additional-label-width ((dialog null) button dims)
(declare (ignore button dims))
0)
(defmethod additional-label-width ((dialog menu) (button dialog-button) dims)
(image-width (ab-vertical-menu-mark-image dims)))
(defmethod additional-label-width ((dialog menu) (button dialog-item) dims)
(image-width (ab-horizontal-menu-mark-image dims)))
(defmethod additional-label-width ((dialog command) button dims)
(declare (ignore dims))
(text-extents (button-font button) "..."))
(defmethod additional-label-width ((dialog confirm) button dims)
(declare (ignore dims))
(text-extents (button-font button) "..."))
(defmethod additional-label-width ((dialog property-sheet) button dims)
(declare (ignore dims))
(text-extents (button-font button) "..."))
(DEFMETHOD display-button-label :after ((button dialog-button) gc)
;; Now draw in the menu-mark at the right end of the button, just to the left of the
;; right-button-end (which leaves right-margin pixels to the right of the mark)
(with-slots (dialog) button
(after-display-button-label dialog button gc)))
(DEFMETHOD display-button-label :after ((item dialog-item) gc)
;; Now draw in the menu-mark at the right end of the button, just to the left of the
;; right-button-end (which leaves right-margin pixels to the right of the mark)
(with-slots (dialog) item
(after-display-button-label dialog item gc)))
(defmethod after-display-button-label ((dialog null) button gc)
(declare (ignore button gc))
nil)
(defmethod after-display-button-label ((dialog menu) (button dialog-button) gc)
(display-menu-mark button gc :below))
(defmethod after-display-button-label ((dialog menu) (item dialog-item) gc)
(display-menu-mark item gc :to-the-right))
(defun display-menu-mark (button gc direction)
(let ((width (contact-width button)))
(LET* ((scale (contact-scale button))
(dims (getf *button-dimensions-by-scale* scale))
(button-pixmaps (get-button-pixmaps button))
(menu-mark-image (ecase direction
(:to-the-right
(ab-horizontal-menu-mark-image dims))
(:below
(ab-vertical-menu-mark-image dims))))
(menu-mark-pixmap (ecase direction
(:to-the-right
(horizontal-menu-mark-pixmap button-pixmaps))
(:below
(vertical-menu-mark-pixmap button-pixmaps))))
(menu-mark-x (- width
(ecase direction
(:below
(ab-right-button-end-width dims))
(:to-the-right
(ai-button-end-width dims)))
(image-width menu-mark-image)))
(menu-mark-y (- (ecase direction
(:below (ab-text-baseline dims))
(:to-the-right (1- (ai-text-baseline dims))))
(image-height menu-mark-image)
;; The 1- for :to-the-right is correction to this.
(ab-menu-mark-bottom-rel-to-baseline dims))))
(with-gcontext (gc :clip-x menu-mark-x
:clip-y menu-mark-y
:clip-mask menu-mark-pixmap)
(draw-rectangle button gc
menu-mark-x menu-mark-y
(image-width menu-mark-image) (image-height menu-mark-image)
t)))))
(defmethod after-display-button-label ((dialog command) button gc)
(display-window-mark button gc))
(defmethod after-display-button-label ((dialog property-sheet) button gc)
(display-window-mark button gc))
(defmethod after-display-button-label ((dialog confirm) button gc)
(display-window-mark button gc))
;; Draw the window mark flush against the right end of the label, using
;; similar computations to those from display-button-label.
(defmethod display-window-mark ((button dialog-button) gc)
(with-slots (font label-alignment label width) button
(let* ((scale (contact-scale button))
(dims (GETF *button-dimensions-by-scale* scale))
(label-width (let ((*inside-display-window-mark* t))
(label-width button label)))
(margin (ab-left-button-end-width dims))
(left-margin (max margin
(case label-alignment
(:left 0)
(:center (pixel-round (- width label-width) 2))
(:right (- width margin label-width)))))
(window-mark-x (+ left-margin label-width 1)) ; Extra pixel looks better.
(window-mark-y (1+ (ab-text-baseline dims))))
(with-gcontext (gc :font font)
(draw-glyphs button gc window-mark-x window-mark-y "...")))))
(defmethod display-window-mark ((item dialog-item) gc)
(with-slots (font label-alignment label width) item
(let* ((scale (contact-scale item))
(dims (GETF *button-dimensions-by-scale* scale))
(label-width (let ((*inside-display-window-mark* t))
(label-width item label)))
(margin (ai-button-end-width dims))
(left-margin (max margin
(case label-alignment
(:left 0)
(:center (pixel-round (- width label-width) 2))
(:right (- width margin label-width)))))
(window-mark-x (+ left-margin label-width 1)) ; Extra pixel looks better.
(window-mark-y (ai-text-baseline dims)))
(with-gcontext (gc :font font)
(draw-glyphs item gc window-mark-x window-mark-y "...")))))
;;;
;;; Position the menu according to the Open Look rules:
;;; For a button, centered horizontally with the top edge against the bottom
;;; edge of the button. For an item, with the default item centered vertically
;;; relative to the item itself. In press-drag-release mode (release-p NIL),
;;; positioned horizontally so the left end of the default item is over the
;;; mouse; in click-move-click mode (release-p T), positioned horizontally so
;;; the left edge of the menu is a pixel away from right edge of the item.
;;;
;;; For pop-ups (not yet implemented), the button will be NIL. In that case,
;;; we align the default item vertically with the mouse, and place the menu so
;;; that the mouse is a pixel or two to the left of the left edge of the default.
(DEFMETHOD set-menu-position ((self dialog-button) menu &optional release-p)
(declare (ignore release-p))
(with-slots (width height x y border-width parent) self
(unless (realized-p menu)
(initialize-geometry menu))
(let ((menu-width (contact-width (contact-parent (menu-choice menu)))))
;; We use the width of the *container* so menu will be
;; centered without considering the drop shadow.
(multiple-value-bind (menu-x menu-y)
(contact-translate
(contact-parent self)
(- (+ x (round width 2)) (round menu-width 2))
(+ y border-width border-width height 1)
(contact-parent menu))
(SETF menu-x (MIN (MAX 0 menu-x)
(- (contact-width (contact-parent menu)) menu-width)))
(change-geometry menu
:x menu-x
:y menu-y
:accept-p t)))))
;; For a dialog-item, the menu comes up to the right, with the default item aligned with
;; the item, center to center. In pdr mode, the X coordinate is such that the left end of the
;; default item is under the pointer; in cmc mode, the left edge of the menu is one pixel
;; to the right of the item.
(DEFMETHOD set-menu-position ((self dialog-item) menu &optional release-p)
(initialize-geometry menu) ; Needed to get correct sizes for Y coord.
(with-slots (width height x y border-width parent) self
(let* ((choice (menu-choice menu))
(default (or (choice-default choice) ; Could be NIL, but Open Look insists on a default.
(first (composite-children choice))))
(default-scale (contact-scale default))
(dims (GETF *button-dimensions-by-scale* default-scale))
(container (contact-parent choice))
(menu-width (contact-width container))
(menu-height (contact-height container)))
;; We use the width of the *container* so menu will be
;; centered without considering the drop shadow.
(multiple-value-bind (default-x default-y)
;; Translate default-item position into offset from menu 0,0.
(contact-translate (contact-parent default)
(contact-x default)
(contact-y default)
menu)
(multiple-value-bind (menu-x menu-y)
(contact-translate (contact-parent self)
(if release-p
(+ x width border-width border-width 2) ; I think that's 1 + 1 for default-ring.
(- (+ (pointer-position self) ; Should be the pointer X.
x)
(ab-left-button-end-width dims)
default-x))
(- (+ y (round height 2)) ; Align the centers in Y.
(+ default-y (round (contact-height default) 2)))
(contact-parent menu))
(setq menu-x (MIN (MAX 0 menu-x)
(- (contact-width (contact-parent menu)) menu-width))
menu-y (MIN (MAX 0 menu-y)
(- (contact-height (contact-parent menu)) menu-height)))
(change-geometry menu
:x menu-x
:y menu-y
:accept-p t))))))
;; For a pop-up menu, there is no item. Bring it up under the mouse, with the default
;; item centered vertically and its left edge a couple of pixels to the right of the mouse.
(defmethod set-menu-position ((self null) menu &optional release-p)
(declare (ignore release-p))
(initialize-geometry menu) ; Needed to get correct sizes for Y coord.
(let* ((choice (menu-choice menu))
(default (or (choice-default choice) ; Could be NIL, but Open Look insists on a default.
(first (composite-children choice))))
(container (contact-parent choice))
(menu-width (contact-width container))
(menu-height (contact-height container)))
;; We use the width of the *container* so menu will be
;; centered without considering the drop shadow.
(multiple-value-bind (pointer-x pointer-y)
(pointer-position (contact-parent menu))
(multiple-value-bind (default-x default-y)
;; Translate default-item position into offset from menu 0,0.
(contact-translate (contact-parent default)
(contact-x default)
(contact-y default)
menu)
(let ((menu-x (- pointer-x (- default-x 2)))
(menu-y (- pointer-y default-y (round (contact-height default) 2))))
(setq menu-x (MIN (MAX 0 menu-x)
(- (contact-width (contact-parent menu)) menu-width))
menu-y (MIN (MAX 0 menu-y)
(- (contact-height (contact-parent menu)) menu-height)))
(change-geometry menu
:x menu-x
:y menu-y
:accept-p t))))))